//  Distance.java
//
//  Author:
//       Antonio J. Nebro <antonio@lcc.uma.es>
//       Juan J. Durillo <durillo@lcc.uma.es>
//
//  Copyright (c) 2011 Antonio J. Nebro, Juan J. Durillo
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.
// 
//  You should have received a copy of the GNU Lesser General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
package jmetal.util;

import jmetal.core.Solution;
import jmetal.core.SolutionSet;
import jmetal.util.comparators.ObjectiveComparator;
import jmetal.util.wrapper.XReal;

/**
 * This class implements some utilities for calculating distances
 */
public class Distance {

    /**
     * Constructor.
     */
    public Distance() {
        //do nothing.
    } // Distance

    /**
     * Returns a matrix with distances between solutions in a
     * <code>SolutionSet</code>.
     *
     * @param solutionSet The <code>SolutionSet</code>.
     * @return a matrix with distances.
     */
    public double[][] distanceMatrix(SolutionSet solutionSet) {
        Solution solutionI, solutionJ;

        //The matrix of distances
        double[][] distance = new double[solutionSet.size()][solutionSet.size()];
        //-> Calculate the distances
        for (int i = 0; i < solutionSet.size(); i++) {
            distance[i][i] = 0.0;
            solutionI = solutionSet.get(i);
            for (int j = i + 1; j < solutionSet.size(); j++) {
                solutionJ = solutionSet.get(j);
                distance[i][j] = this.distanceBetweenObjectives(solutionI, solutionJ);
                distance[j][i] = distance[i][j];
            } // for
        } // for        

        //->Return the matrix of distances
        return distance;
    } // distanceMatrix

    /**
     * Returns the minimum distance from a <code>Solution</code> to a
     * <code>SolutionSet according to the objective values</code>.
     *
     * @param solution The <code>Solution</code>.
     * @param solutionSet The <code>SolutionSet</code>.
     * @return The minimum distance between solution and the set.
     * @throws JMException
     */
    public double distanceToSolutionSetInObjectiveSpace(Solution solution,
            SolutionSet solutionSet) throws JMException {
        //At start point the distance is the max
        double distance = Double.MAX_VALUE;

        // found the min distance respect to population
        for (int i = 0; i < solutionSet.size(); i++) {
            double aux = this.distanceBetweenObjectives(solution, solutionSet.get(i));
            if (aux < distance) {
                distance = aux;
            }
        } // for

        //->Return the best distance
        return distance;
    } // distanceToSolutionSetinObjectiveSpace

    /**
     * Returns the minimum distance from a <code>Solution</code> to a
     * <code>SolutionSet according to the encodings.variable values</code>.
     *
     * @param solution The <code>Solution</code>.
     * @param solutionSet The <code>SolutionSet</code>.
     * @return The minimum distance between solution and the set.
     * @throws JMException
     */
    public double distanceToSolutionSetInSolutionSpace(Solution solution,
            SolutionSet solutionSet) throws JMException {
        //At start point the distance is the max
        double distance = Double.MAX_VALUE;

        // found the min distance respect to population
        for (int i = 0; i < solutionSet.size(); i++) {
            double aux = this.distanceBetweenSolutions(solution, solutionSet.get(i));
            if (aux < distance) {
                distance = aux;
            }
        } // for

        //->Return the best distance
        return distance;
    } // distanceToSolutionSetInSolutionSpace

    /**
     * Returns the distance between two solutions in the search space.
     *
     * @param solutionI The first <code>Solution</code>.
     * @param solutionJ The second <code>Solution</code>.
     * @return the distance between solutions.
     * @throws JMException
     */
    public double distanceBetweenSolutions(Solution solutionI, Solution solutionJ)
            throws JMException {
        /*
         double distance = 0.0;
         if ((solutionI.getDecisionVariables() != null) &&
         (solutionJ.getDecisionVariables() != null)) {
         Variable[] decisionVariableI = solutionI.getDecisionVariables();
         Variable[] decisionVariableJ = solutionJ.getDecisionVariables();

         double diff;    //Auxiliar var
         //-> Calculate the Euclidean distance
         for (int i = 0; i < decisionVariableI.length; i++){
         diff = decisionVariableI[i].getValue() -
         decisionVariableJ[i].getValue();
         distance += Math.pow(diff,2.0);
         } // for    
         }
         //-> Return the euclidean distance
         return Math.sqrt(distance);
         */
        double distance = 0.0;
        XReal solI = new XReal(solutionI);
        XReal solJ = new XReal(solutionJ);

        double diff;    //Auxiliar var
        //-> Calculate the Euclidean distance
        for (int i = 0; i < solI.getNumberOfDecisionVariables(); i++) {
            diff = solI.getValue(i) - solJ.getValue(i);
            distance += Math.pow(diff, 2.0);
        } // for
        //-> Return the euclidean distance
        return Math.sqrt(distance);
    } // distanceBetweenSolutions

    /**
     * Returns the distance between two solutions in objective space.
     *
     * @param solutionI The first <code>Solution</code>.
     * @param solutionJ The second <code>Solution</code>.
     * @return the distance between solutions in objective space.
     */
    public double distanceBetweenObjectives(Solution solutionI, Solution solutionJ) {
        double diff;    //Auxiliar var
        double distance = 0.0;
        //-> Calculate the euclidean distance
        for (int nObj = 0; nObj < solutionI.getNumberOfObjectives(); nObj++) {
            diff = solutionI.getObjective(nObj) - solutionJ.getObjective(nObj);
            distance += Math.pow(diff, 2.0);
        } // for   

        //Return the euclidean distance
        return Math.sqrt(distance);
    } // distanceBetweenObjectives.

    /**
     * Return the index of the nearest solution in the solution set to a given
     * solution
     *
     * @param solution
     * @param solutionSet
     * @return The index of the nearest solution; -1 if the solutionSet is empty
     */
    public int indexToNearestSolutionInSolutionSpace(Solution solution, SolutionSet solutionSet) {
        int index = -1;
        double minimumDistance = Double.MAX_VALUE;
        try {
            for (int i = 0; i < solutionSet.size(); i++) {
                double distance = 0;
                distance = distanceBetweenSolutions(solution, solutionSet.get(i));
                if (distance < minimumDistance) {
                    minimumDistance = distance;
                    index = i;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return index;
    }

    /**
     * Assigns crowding distances to all solutions in a
     * <code>SolutionSet</code>.
     *
     * @param solutionSet The <code>SolutionSet</code>.
     * @param nObjs Number of objectives.
     */
    public void crowdingDistanceAssignment(SolutionSet solutionSet, int nObjs) {
        int size = solutionSet.size();

        if (size == 0) {
            return;
        }
        if (size == 1) {
            solutionSet.get(0).setCrowdingDistance(Double.POSITIVE_INFINITY);
            return;
        } // if

        if (size == 2) {
            solutionSet.get(0).setCrowdingDistance(Double.POSITIVE_INFINITY);
            solutionSet.get(1).setCrowdingDistance(Double.POSITIVE_INFINITY);
            return;
        } // if       

        //Use a new SolutionSet to evite alter original solutionSet
        SolutionSet front = new SolutionSet(size);
        for (int i = 0; i < size; i++) {
            front.add(solutionSet.get(i));
        }
        /*Limpa o valor do atributo crowdingDistance*/
        for (int i = 0; i < size; i++) {
            front.get(i).setCrowdingDistance(0.0);
        }

        double objetiveMinn;
        double objetiveMaxn;
        double distance;

        for (int i = 0; i < nObjs; i++) {
            /* Ordena a populacao obtendo o menor e maior valor de distancia
            armazenado em cada objetivo */
            front.sort(new ObjectiveComparator(i));
            objetiveMinn = front.get(0).getObjective(i);
            objetiveMaxn = front.get(front.size() - 1).getObjective(i);

            //Set de crowding distance            
            front.get(0).setCrowdingDistance(Double.POSITIVE_INFINITY);
            front.get(size - 1).setCrowdingDistance(Double.POSITIVE_INFINITY);

            /*Calcula a distancia de multidao para cada individuo.
            O calculo é feito baseado nas distancia do vizinho anterio e posterior
            levando em consideracao todos os objetivos (um por vez)*/
            for (int j = 1; j < size - 1; j++) {
                distance = front.get(j + 1).getObjective(i) - front.get(j - 1).getObjective(i);
                distance = distance / (objetiveMaxn - objetiveMinn);
                distance += front.get(j).getCrowdingDistance();
                front.get(j).setCrowdingDistance(distance);
            } // for
        } // for        
    } // crowdingDistanceAssing            
} // Distance

